/* Copyright 2018 SiFive, Inc */
/* SPDX-License-Identifier: Apache-2.0 */

#include <metal/machine/platform.h>

#ifdef METAL_RISCV_CLINT0

#include <metal/io.h>
#include <metal/cpu.h>
#include <metal/drivers/riscv_clint0.h>
#include <metal/machine.h>

unsigned long long __metal_clint0_mtime_get (struct __metal_driver_riscv_clint0 *clint)
{
    __metal_io_u32 lo, hi;
    unsigned long control_base = __metal_driver_sifive_clint0_control_base(&clint->controller);

    /* Guard against rollover when reading */
    do {
	hi = __METAL_ACCESS_ONCE((__metal_io_u32 *)(control_base + METAL_RISCV_CLINT0_MTIME + 4));
	lo = __METAL_ACCESS_ONCE((__metal_io_u32 *)(control_base + METAL_RISCV_CLINT0_MTIME));
    } while (__METAL_ACCESS_ONCE((__metal_io_u32 *)(control_base + METAL_RISCV_CLINT0_MTIME + 4)) != hi);

    return (((unsigned long long)hi) << 32) | lo;
}

int __metal_driver_riscv_clint0_mtimecmp_set(struct metal_interrupt *controller,
                                             int hartid,
                                             unsigned long long time)
{   
    struct __metal_driver_riscv_clint0 *clint =
                              (struct __metal_driver_riscv_clint0 *)(controller);
    unsigned long control_base = __metal_driver_sifive_clint0_control_base(&clint->controller);
    /* Per spec, the RISC-V MTIME/MTIMECMP registers are 64 bit,
     * and are NOT internally latched for multiword transfers.
     * Need to be careful about sequencing to avoid triggering
     * spurious interrupts: For that set the high word to a max
     * value first.
     */
    __METAL_ACCESS_ONCE((__metal_io_u32 *)(control_base + (8 * hartid) + METAL_RISCV_CLINT0_MTIMECMP_BASE + 4)) = 0xFFFFFFFF;
    __METAL_ACCESS_ONCE((__metal_io_u32 *)(control_base + (8 * hartid) + METAL_RISCV_CLINT0_MTIMECMP_BASE)) = (__metal_io_u32)time;
    __METAL_ACCESS_ONCE((__metal_io_u32 *)(control_base + (8 * hartid) + METAL_RISCV_CLINT0_MTIMECMP_BASE + 4)) = (__metal_io_u32)(time >> 32);
    return 0;
}

static struct metal_interrupt *_get_cpu_intc()
{
    int hartid = 0;
    __asm__ volatile("csrr %[hartid], mhartid"
                     : [hartid] "=r" (hartid) :: "memory");

    struct metal_cpu *cpu = metal_cpu_get(hartid);

    return metal_cpu_interrupt_controller(cpu);
}

void __metal_driver_riscv_clint0_init (struct metal_interrupt *controller)
{
    int num_interrupts = __metal_driver_sifive_clint0_num_interrupts(controller);
    struct __metal_driver_riscv_clint0 *clint =
                              (struct __metal_driver_riscv_clint0 *)(controller);

    if ( !clint->init_done ) {
	/* Register its interrupts with with parent controller, aka sw and timerto its default isr */
        for (int i = 0; i < num_interrupts; i++) {
	  struct metal_interrupt *intc = __metal_driver_sifive_clint0_interrupt_parents(controller, i);
	  int line = __metal_driver_sifive_clint0_interrupt_lines(controller, i);
            intc->vtable->interrupt_register(intc, line, NULL, controller);
	}
	clint->init_done = 1;
    }	
}

int __metal_driver_riscv_clint0_register (struct metal_interrupt *controller,
                                        int id, metal_interrupt_handler_t isr,
                                        void *priv)
{
    int rc = -1;
    metal_vector_mode mode = __metal_controller_interrupt_vector_mode();
    struct metal_interrupt *intc = NULL;
    struct metal_interrupt *cpu_intc = _get_cpu_intc();
    int num_interrupts = __metal_driver_sifive_clint0_num_interrupts(controller);

    if ( (mode != METAL_VECTOR_MODE) && (mode != METAL_DIRECT_MODE) ) {
        return rc;
    }

    for(int i = 0; i < num_interrupts; i++) {
	int line = __metal_driver_sifive_clint0_interrupt_lines(controller, i);
        intc = __metal_driver_sifive_clint0_interrupt_parents(controller, i);
        if (cpu_intc == intc && id == line) {
            break;
        }
	intc = NULL;
    }

    /* Register its interrupts with parent controller */
    if (intc) {
        rc = intc->vtable->interrupt_register(intc, id, isr, priv);
    }
    return rc;
}

int __metal_driver_riscv_clint0_vector_register (struct metal_interrupt *controller,
                                                 int id, metal_interrupt_vector_handler_t isr,
                                                 void *priv)
{   
    /* Not supported. User can override the 'weak' handler with their own */
    int rc = -1;
    return rc;
}

metal_vector_mode __metal_driver_riscv_clint0_get_vector_mode (struct metal_interrupt *controller)
{
    return __metal_controller_interrupt_vector_mode();
}

int __metal_driver_riscv_clint0_set_vector_mode (struct metal_interrupt *controller, metal_vector_mode mode)
{
    int rc = -1;
    struct metal_interrupt *intc = _get_cpu_intc();

    if (intc) {
	/* Valid vector modes are VECTOR and DIRECT, anything else is invalid (-1) */
        switch (mode) {
        case METAL_VECTOR_MODE:
        case METAL_DIRECT_MODE:
            rc = intc->vtable->interrupt_set_vector_mode(intc, mode);
            break;
        case METAL_HARDWARE_VECTOR_MODE:
        case METAL_SELECTIVE_NONVECTOR_MODE:
        case METAL_SELECTIVE_VECTOR_MODE:
        break;
        }
    }
    return rc;
}

int __metal_driver_riscv_clint0_enable (struct metal_interrupt *controller, int id)
{
    int rc = -1;

    if ( id ) {
        struct metal_interrupt *intc = NULL;
        struct metal_interrupt *cpu_intc = _get_cpu_intc();
	int num_interrupts = __metal_driver_sifive_clint0_num_interrupts(controller);

        for(int i = 0; i < num_interrupts; i++) {
	    int line = __metal_driver_sifive_clint0_interrupt_lines(controller, i);
	    intc = __metal_driver_sifive_clint0_interrupt_parents(controller, i);
            if(cpu_intc == intc && id == line) {
                break;
            }
	    intc = NULL;
        }
        
        /* Enable its interrupts with parent controller */
        if (intc) {
            rc = intc->vtable->interrupt_enable(intc, id);
        }
    }

    return rc;
}

int __metal_driver_riscv_clint0_disable (struct metal_interrupt *controller, int id)
{
    int rc = -1;

    if ( id ) {
        struct metal_interrupt *intc = NULL;
        struct metal_interrupt *cpu_intc = _get_cpu_intc();
	int num_interrupts = __metal_driver_sifive_clint0_num_interrupts(controller);

        for(int i = 0; i < num_interrupts; i++) {
	    int line = __metal_driver_sifive_clint0_interrupt_lines(controller, i);
	    intc = __metal_driver_sifive_clint0_interrupt_parents(controller, i);
            if(cpu_intc == intc && id == line) {
                break;
            }
	    intc = NULL;
        }
        
        /* Disable its interrupts with parent controller */
        if (intc) {
            rc = intc->vtable->interrupt_disable(intc, id);
        }
    }

    return rc;
}

int __metal_driver_riscv_clint0_command_request (struct metal_interrupt *controller,
                                               int command, void *data)
{
    int hartid;
    int rc = -1;
    struct __metal_driver_riscv_clint0 *clint =
                              (struct __metal_driver_riscv_clint0 *)(controller);
    unsigned long control_base = __metal_driver_sifive_clint0_control_base(controller);

    switch (command) {
    case METAL_TIMER_MTIME_GET:
        if (data) {
	    *(unsigned long long *)data = __metal_clint0_mtime_get(clint);
            rc = 0;
        }
        break;
    case METAL_SOFTWARE_IPI_CLEAR:
	if (data) {
	    hartid = *(int *)data;
            __METAL_ACCESS_ONCE((__metal_io_u32 *)(control_base +
					       (hartid * 4))) = METAL_DISABLE;
            rc = 0;
        }
        break;
    case METAL_SOFTWARE_IPI_SET:
	if (data) {
	    hartid = *(int *)data;
            __METAL_ACCESS_ONCE((__metal_io_u32 *)(control_base +
					       (hartid * 4))) = METAL_ENABLE;
	    /* Callers of this function assume it's blocking, in the sense that
	     * the IPI is guarnteed to have been delivered before the function
	     * returns.  We can't really guarnteed it's delivered, but we can
	     * read back the control register after writing it in at least an
	     * attempt to provide some semblence of ordering here.  The fence
	     * ensures the read is order after the write -- it wouldn't be
	     * necessary under RVWMO because this is the same address, but we
	     * don't have an IO memory model so I'm being a bit overkill here.
	     */
	    __METAL_IO_FENCE(o,i);
            rc = __METAL_ACCESS_ONCE((__metal_io_u32 *)(control_base +
					       (hartid * 4)));
            rc = 0;
        }
        break;
    case METAL_SOFTWARE_MSIP_GET:
        rc = 0;
	if (data) {
	    hartid = *(int *)data;
            rc = __METAL_ACCESS_ONCE((__metal_io_u32 *)(control_base +
						    (hartid * 4)));
        }
        break;
    default:
	break;
    }

    return rc;
}

int __metal_driver_riscv_clint0_clear_interrupt (struct metal_interrupt *controller, int id)
{
    int hartid = metal_cpu_get_current_hartid();
    return __metal_driver_riscv_clint0_command_request(controller,
						METAL_SOFTWARE_IPI_CLEAR, &hartid);
}

int __metal_driver_riscv_clint0_set_interrupt (struct metal_interrupt *controller, int id)
{
    int hartid = metal_cpu_get_current_hartid();
    return __metal_driver_riscv_clint0_command_request(controller,
						METAL_SOFTWARE_IPI_SET, &hartid);
}


__METAL_DEFINE_VTABLE(__metal_driver_vtable_riscv_clint0) = {
    .clint_vtable.interrupt_init     = __metal_driver_riscv_clint0_init,
    .clint_vtable.interrupt_register = __metal_driver_riscv_clint0_register,
    .clint_vtable.interrupt_vector_register = __metal_driver_riscv_clint0_vector_register,
    .clint_vtable.interrupt_enable   = __metal_driver_riscv_clint0_enable,
    .clint_vtable.interrupt_disable  = __metal_driver_riscv_clint0_disable,
    .clint_vtable.interrupt_get_vector_mode = __metal_driver_riscv_clint0_get_vector_mode,
    .clint_vtable.interrupt_set_vector_mode = __metal_driver_riscv_clint0_set_vector_mode,
    .clint_vtable.interrupt_clear    = __metal_driver_riscv_clint0_clear_interrupt,
    .clint_vtable.interrupt_set      = __metal_driver_riscv_clint0_set_interrupt,
    .clint_vtable.command_request    = __metal_driver_riscv_clint0_command_request,
    .clint_vtable.mtimecmp_set       = __metal_driver_riscv_clint0_mtimecmp_set,
};

#endif /* METAL_RISCV_CLINT0 */

typedef int no_empty_translation_units;
